/*******************************************************************************
 * Copyright (c) 2008, 2016 Wind River Systems and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     Wind River Systems - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.examples.pdavm;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.StringWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 
 */
@SuppressWarnings("serial")
public class PDAVirtualMachine {

    static class Stack extends LinkedList<Object> {
        @Override
	public Object pop() {
            return isEmpty() ? 0 : remove(size() - 1);
        }

        @Override
	public void push(Object value) {
            add(value);
        }
    }
    
    static class Register {
        Register(String name) { 
            fName = name; 
        }
        String fName;
        String fGroup = "<no_group>";
        boolean fIsWriteable = true;
        Map<String, BitField> fBitFields = new LinkedHashMap<String, BitField>(0);
        int fValue;
    }

    static class BitField {
        BitField(String name) { 
            fName = name; 
        }
        String fName;
        int fBitOffset;
        int fBitCount;
        Map<String, Integer> fMnemonics = new LinkedHashMap<String, Integer>(0);
    }

    Map<String,Register> fRegisters = new LinkedHashMap<String,Register>(0);
    
    class Args {
        final String[] fArgs;

        int next = 0;

        Args(String[] args) {
            fArgs = args;
        }

        String getNextStringArg() {
            if (fArgs.length > next) {
                return fArgs[next++];
            }
            return "";
        }

        int getNextIntArg() {
            String arg = getNextStringArg();
            try {
                return Integer.parseInt(arg);
            } catch (NumberFormatException e) {
            }
            return 0;
        }

        boolean getNextBooleanArg() {
            String arg = getNextStringArg();
            try {
                return Boolean.parseBoolean(arg);
            } catch (NumberFormatException e) {
            }
            return false;
        }

        Object getNextIntOrStringArg() {
            String arg = getNextStringArg();
            try {
                return Integer.parseInt(arg);
            } catch (NumberFormatException e) {
            }
            return arg;
        }

        PDAThread getThreadArg() {
            int id = getNextIntArg();
            return fThreads.get(id);
        }
    }

    class PDAThread {
        final int fID;

        /** The push down automata data stack (the data stack). */
        final Stack fStack = new Stack();

        /**
         * PDAThread copy of the code. It can differ from the program if
         * performing an evaluation.
         */
        String[] fThreadCode;

        /** PDAThread copy of the labels. */
        Map<String, Integer> fThreadLabels;

        /** The stack of stack frames (the control stack) */
        final List<Frame> fFrames = new LinkedList<Frame>();

        /** Current stack frame (not includced in fFrames) */
        Frame fCurrentFrame;

        /**
         * The run flag is true if the thread is running. If the run flag is
         * false, the thread exits the next time the main instruction loop runs.
         */
        boolean fRun = true;

        String fSuspend = null;

        boolean fStep = false;

        boolean fStepReturn = false;
        
        int fSavedPC;

        boolean fPerformingEval = false;
        
        PDAThread(int id, String function, int pc) {
            fID = id;
            fCurrentFrame = new Frame(function, pc);
            fThreadCode = fCode;
            fThreadLabels = fLabels;
        }
    }

    final Map<Integer, PDAThread> fThreads = new LinkedHashMap<Integer, PDAThread>();

    int fNextThreadId = 1;

    boolean fStarted = true;
    /**
     * The code is stored as an array of strings, each line of the source file
     * being one entry in the array.
     */
    final String[] fCode;

    /** A mapping of labels to indicies in the code array */
    final Map<String, Integer> fLabels;

    /** Each stack frame is a mapping of variable names to values. */
    class Frame {
        final Map<String, Object> fLocalVariables = new LinkedHashMap<String, Object>();
        
        /**
         * The name of the function in this frame
         */
        final String fFunction;

        /**
         * The current program counter in the frame the pc points to the next
         * instruction to be executed
         */
        int fPC;

        Frame(String function, int pc) {
            fFunction = function;
            fPC = pc;
        }
        
        private int parseArrayParam(String name) {
            int lb = name.indexOf('[');
            int rb = name.indexOf(']', lb);
            if (rb < 0) return -1;
            String paramStr = name.substring(lb + 1, rb);
            if (paramStr.length() == 0) return -1;
            
            try {
                return Integer.parseInt(name.substring(lb + 1, rb));
            } catch (NumberFormatException e) {} // Not a number
            
            Object paramVar = fLocalVariables.get(paramStr);
            if (paramVar instanceof Integer) {
                return (Integer)paramVar;
            }
            return -1;
        }
        
        void declare(String name) {
            Object value = Integer.valueOf(0);
            if (name.startsWith("$")) {
                setRegisterValue(name, value);
            } else if (name.indexOf('[') >= 0) {
                int size = parseArrayParam(name);
                if (size >= 0) {
                    String arrayName = name.substring(0, name.indexOf('['));
                    Object[] array = new Object[size];
                    for (int i = 0; i < size; i++) {
                        array[i] = value;
                    }
                    fLocalVariables.put(arrayName, array);
                }
            } else {
                fLocalVariables.put(name, value);
            }
        }
        
        void set(String name, Object value) {
            if (name.startsWith("$")) {
                setRegisterValue(name, value);
            } else if (name.indexOf('[') >= 0) {
                int index = parseArrayParam(name);
                if (index >=0) {
                    String arrayName = name.substring(0, name.indexOf('['));
                    Object array = fLocalVariables.get(arrayName);
                    if (array instanceof Object[] && ((Object[])array).length > index) {
                        ((Object[])array)[index] = value;
                    }
                }
            } else {
                fLocalVariables.put(name, value);
            }
        }
        
        Object get(String name) {
            if (name.startsWith("$")) {
                return getRegisterValue(name);
            } else if (name.indexOf('[') >= 0) {
                int index = parseArrayParam(name);
                if (index >= 0) {
                    String arrayName = name.substring(0, name.indexOf('['));
                    Object array = fLocalVariables.get(arrayName);
                    if (array instanceof Object[] && ((Object[])array).length > index) {
                        return ((Object[])array)[index];
                    }
                }
                return null;
            } else { 
                return fLocalVariables.get(name);
            }
        }
    }

    void setRegisterValue(String name, Object value) {
        Register reg = fRegisters.get(getRegisterPartOfName(name));
        if (reg == null) return;
        String bitFieldName = getBitFieldPartOfName(name);
        if (bitFieldName != null) {
            BitField bitField = reg.fBitFields.get(bitFieldName);
            if (bitField == null) return;
            Integer intValue = null;
            if (value instanceof Integer) {
                intValue = (Integer)value;
            } else if (value instanceof String) {
                intValue = bitField.fMnemonics.get(value);
            }
            if (intValue != null) {
                int bitFieldMask = 2^(bitField.fBitCount - 1);           
                int registerMask = ~(bitFieldMask << bitField.fBitOffset);
                int bitFieldValue = intValue & bitFieldMask;
                reg.fValue = (reg.fValue & registerMask) | (bitFieldValue << bitField.fBitOffset);
            }
        } else if (value instanceof Integer) {
            reg.fValue = ((Integer)value).intValue();
        }
    }

    Object getRegisterValue(String name) {
        Register reg = fRegisters.get(getRegisterPartOfName(name));
        if (reg == null) return null;
        String bitFieldName = getBitFieldPartOfName(name);
        if (bitFieldName != null) {
            BitField bitField = reg.fBitFields.get(bitFieldName);
            if (bitField == null) return null;
            int bitFieldMask = 2^(bitField.fBitCount - 1);           
            int registerMask = bitFieldMask << bitField.fBitOffset;
            return (reg.fValue & registerMask) >> bitField.fBitOffset;
        } else {
            return reg.fValue;
        }
    }
    
    /**
     * Breakpoints are stored per each each line of code.  The boolean indicates
     * whether the whole VM should suspend or just the triggering thread.
     */
    final Map<Integer, Boolean> fBreakpoints = new HashMap<Integer, Boolean>();

    /**
     * The suspend flag is true if the VM should suspend running the program and
     * just listen for debug commands.
     */
    String fSuspendVM;

    /** Flag indicating whether the debugger is performing a step. */
    boolean fStepVM = false;

    /** Flag indicating whether the debugger is performing a step return */
    boolean fStepReturnVM = false;
    
    int fSteppingThread = 0;

    /** Name of the pda program being debugged */
    final String fFilename;

    /** The command line argument to start a debug session. */
    final boolean fDebug;

    /** The port to listen for debug commands on */
    final int fCommandPort;

    /**
     * Command socket for receiving debug commands and sending command responses
     */
    Socket fCommandSocket;

    /** Command socket reader */
    BufferedReader fCommandReceiveStream;

    /** Command socket write stream. */
    OutputStream fCommandResponseStream;

    /** The port to send debug events to */
    final int fEventPort;

    /** Event socket */
    Socket fEventSocket;

    /** Event socket and write stream. */
    OutputStream fEventStream;

    /** The eventstops table holds which events cause suspends and which do not. */
    final Map<String, Boolean> fEventStops = new HashMap<String, Boolean>();
    {
        fEventStops.put("unimpinstr", false);
        fEventStops.put("nosuchlabel", false);
    }

    /**
     * The watchpoints table holds watchpoint information.
     * <p/>
     * variablename_stackframedepth => N
     * <ul> 
     * <li>N = 0 is no watch</li> 
     * <li>N = 1 is read watch</li>
     * <li>N = 2 is write watch</li>
     * <li>N = 3 is both, etc.</li>
     */
    final Map<String, Integer> fWatchpoints = new HashMap<String, Integer>();

    public static void main(String[] args) {
        String programFile = args.length >= 1 ? args[0] : null;
        if (programFile == null) {
            System.err.println("Error: No program specified");
            return;
        }

        String debugFlag = args.length >= 2 ? args[1] : "";
        boolean debug = "-debug".equals(debugFlag);
        int commandPort = 0;
        int eventPort = 0;

        if (debug) {
            String commandPortStr = args.length >= 3 ? args[2] : "";
            try {
                commandPort = Integer.parseInt(commandPortStr);
            } catch (NumberFormatException e) {
                System.err.println("Error: Invalid command port");
                return;
            }

            String eventPortStr = args.length >= 4 ? args[3] : "";
            try {
                eventPort = Integer.parseInt(eventPortStr);
            } catch (NumberFormatException e) {
                System.err.println("Error: Invalid event port");
                return;
            }
        }

        PDAVirtualMachine pdaVM = null;
        try {
            pdaVM = new PDAVirtualMachine(programFile, debug, commandPort, eventPort);
            pdaVM.startDebugger();
        } catch (IOException e) {
            System.err.println("Error: " + e.toString());
            return;
        }
        pdaVM.run();
    }

	PDAVirtualMachine(String inputFile, boolean debug, int commandPort, int eventPort) throws IOException {
		fFilename = inputFile;

		// Load all the code into memory
		try (FileReader fileReader = new FileReader(inputFile)) {
			StringWriter stringWriter = new StringWriter();
			List<String> code = new LinkedList<String>();
			int c = fileReader.read();
			while (c != -1) {
				if (c == '\n') {
					code.add(stringWriter.toString().trim());
					stringWriter = new StringWriter();
				} else {
					stringWriter.write(c);
				}
				c = fileReader.read();
			}
			code.add(stringWriter.toString().trim());
			fCode = code.toArray(new String[code.size()]);

			fLabels = mapLabels(fCode);

			fDebug = debug;
			fCommandPort = commandPort;
			fEventPort = eventPort;
		}
	}

    /**
     * Initializes the labels map
     */
    Map<String, Integer> mapLabels(String[] code) {
        Map<String, Integer> labels = new HashMap<String, Integer>();
        for (int i = 0; i < code.length; i++) {
            if (code[i].length() != 0 && code[i].charAt(0) == ':') {
                labels.put(code[i].substring(1), i);
            }
        }
        return labels;
    }

    void sendCommandResponse(String response) {
        try {
            fCommandResponseStream.write(response.getBytes());
            fCommandResponseStream.flush();
        } catch (IOException e) {
        }
    }

    void sendDebugEvent(String event, boolean error) {
        if (fDebug) {
            try {
                fEventStream.write(event.getBytes());
                fEventStream.write('\n');
                fEventStream.flush();
            } catch (IOException e) {
                System.err.println("Error: " + e);
                System.exit(1);
            }
        } else if (error) {
            System.err.println("Error: " + event);
        }
    }

    void startDebugger() throws IOException {
        if (fDebug) {
            System.out.println("-debug " + fCommandPort + " " + fEventPort);
        }

        ServerSocket commandServerSocket = new ServerSocket(fCommandPort);
        fCommandSocket = commandServerSocket.accept();
        fCommandReceiveStream = new BufferedReader(new InputStreamReader(fCommandSocket.getInputStream()));
        fCommandResponseStream = new PrintStream(fCommandSocket.getOutputStream());
        commandServerSocket.close();

        ServerSocket eventServerSocket = new ServerSocket(fEventPort);
        fEventSocket = eventServerSocket.accept();
        fEventStream = new PrintStream(fEventSocket.getOutputStream());
        eventServerSocket.close();

        System.out.println("debug connection accepted");

        fSuspendVM = "client";
    }

    void run() {
        int id = fNextThreadId++;
        fThreads.put(id, new PDAThread(id, "main", 0));
        if (fDebug) {
            sendDebugEvent("started " + id, false);
        }

        boolean allThreadsSuspended = false;
        while (!fThreads.isEmpty()) {
            checkForBreakpoint();
            
            if (fSuspendVM != null) {
                debugUI();
            } else {
                yieldToDebug(allThreadsSuspended);
                if (fSuspendVM != null) {
                    // Received a command to suspend VM, skip executing threads.
                    continue;
                }
            }

            PDAThread[] threadsCopy = fThreads.values().toArray(new PDAThread[fThreads.size()]);
            allThreadsSuspended = true;
            for (PDAThread thread : threadsCopy) {
                if (thread.fSuspend == null) {
                    allThreadsSuspended = false;
                    
                    String instruction = thread.fThreadCode[thread.fCurrentFrame.fPC];
                    thread.fCurrentFrame.fPC++;
                    doOneInstruction(thread, instruction);
                    if (thread.fCurrentFrame.fPC >= thread.fThreadCode.length) {
                        // Thread reached end of code, exit from the thread.
                        thread.fRun = false;
                    } else if (thread.fStepReturn) {
                        // If this thread is in a step-return operation, check
                        // if we've returned from a call.  
                        instruction = thread.fThreadCode[thread.fCurrentFrame.fPC];
                        if ("return".equals(instruction)) {
                            // Note: this will only be triggered if the current 
                            // thread also has the fStepReturn flag set.
                            if (fStepReturnVM) {
                                fSuspendVM = thread.fID + " step";
                            } else {
                                thread.fSuspend = "step";
                            }
                        }
                    }
                    if (!thread.fRun) {
                        sendDebugEvent("exited " + thread.fID, false);
                        fThreads.remove(thread.fID);
                    } else if (thread.fSuspend != null) {
                        sendDebugEvent("suspended " + thread.fID + " " + thread.fSuspend, false);
                        thread.fStep = thread.fStepReturn = thread.fPerformingEval = false;
                    }
                } 
            }
            
            // Force thread context switch to avoid starving out other
            // processes in the system.
            Thread.yield();
        }
        
        sendDebugEvent("terminated", false);
        if (fDebug) {
            try {
                fCommandReceiveStream.close();
                fCommandResponseStream.close();
                fCommandSocket.close();
                fEventStream.close();
                fEventSocket.close();
            } catch (IOException e) {
                System.out.println("Error: " + e);
            }
        }

    }

    void doOneInstruction(PDAThread thread, String instr) {
        StringTokenizer tokenizer = new StringTokenizer(instr);
        String op = tokenizer.nextToken();
        List<String> tokens = new LinkedList<String>();
        while (tokenizer.hasMoreTokens()) {
            tokens.add(tokenizer.nextToken());
        }
        Args args = new Args(tokens.toArray(new String[tokens.size()]));

        boolean opValid = true;
        if (op.equals("add")) iAdd(thread, args);
        else if (op.equals("branch_not_zero")) iBranchNotZero(thread, args);
        else if (op.equals("call")) iCall(thread, args);
        else if (op.equals("dec")) iDec(thread, args);
        else if (op.equals("def")) iDef(thread, args);
        else if (op.equals("dup")) iDup(thread, args);
        else if (op.equals("exec")) iExec(thread, args);            
        else if (op.equals("halt")) iHalt(thread, args);
        else if (op.equals("output")) iOutput(thread, args);
        else if (op.equals("pop")) iPop(thread, args);
        else if (op.equals("push")) iPush(thread, args);
        else if (op.equals("return")) iReturn(thread, args);
        else if (op.equals("var")) iVar(thread, args);
        else if (op.equals("xyzzy")) iInternalEndEval(thread, args);
        else if (op.startsWith(":")) {} // label
        else if (op.startsWith("#")) {} // comment
        else {
            opValid = false;
        }

        if (!opValid) {
            sendDebugEvent("unimplemented instruction " + op, true);
            if (fEventStops.get("unimpinstr")) {
                fSuspendVM = thread.fID + " event unimpinstr";
                thread.fCurrentFrame.fPC--;
            }
        } else if (thread.fStep) {
            if (fStepVM) {
                fSuspendVM = thread.fID + " step";
                fStepVM = false;
            } else {
                thread.fSuspend = "step";
            }
            thread.fStep = false;
        }
    }

    void checkForBreakpoint() {
        if (fDebug) {
            for (PDAThread thread : fThreads.values()) {
                int pc = thread.fCurrentFrame.fPC;
                // Suspend for breakpoint if:
                // - the VM is not yet set to suspend, for e.g. as a result of step end,
                // - the thread is not yet suspended and is not performing an evaluation
                // - the breakpoints table contains a breakpoint for the given line.
                if (fSuspendVM == null && 
                    thread.fSuspend == null && !thread.fPerformingEval && 
                    fBreakpoints.containsKey(pc)) 
                {
                    if (fBreakpoints.get(pc)) {
                        fSuspendVM = thread.fID + " breakpoint " + pc;
                    } else {
                        thread.fSuspend = "breakpoint " + pc;
                        thread.fStep = thread.fStepReturn = false;
                        sendDebugEvent("suspended " + thread.fID + " " + thread.fSuspend, false);
                    }
                }
            }
        }
    }

    /**
     * After each instruction, we check the debug command channel for control input. If
     * there are commands, process them.
     */
    void yieldToDebug(boolean allThreadsSuspended) {
        if (fDebug) {
            String line = "";
            try {
                if (allThreadsSuspended || fCommandReceiveStream.ready()) {
                    line = fCommandReceiveStream.readLine();
                    if (line == null)
                    	return;
                    
                    processDebugCommand(line);
                }
            } catch (IOException e) {
                System.err.println("Error: " + e);
                System.exit(1);
            }
        }
    }

    /**
     *  Service the debugger commands while the VM is suspended
     */
    void debugUI() {
        if (!fStarted) {
            sendDebugEvent("vmsuspended " + fSuspendVM, false);
        } else {
            fStarted = false;
        }

        // Clear all stepping flags.  In case the VM suspended while
        // a step operation was being performed for the VM or some thread.
        fStepVM = fStepReturnVM = false;
        for (PDAThread thread : fThreads.values()) {
            thread.fSuspend = null;
            thread.fStep = thread.fStepReturn = thread.fPerformingEval = false;
        }
        
        while (fSuspendVM != null) {
            String line = "";
            try {
                line = fCommandReceiveStream.readLine();
            } catch (IOException e) {
                System.err.println("Error: " + e);
                System.exit(1);
                return;
            }
            
            if (line == null)
            	break;
            
            processDebugCommand(line);
        }

        if (fStepVM || fStepReturnVM) {
            sendDebugEvent("vmresumed step", false);
        } else {
            sendDebugEvent("vmresumed client", false);
        }
    }

    void processDebugCommand(String line) {
        StringTokenizer tokenizer = new StringTokenizer(line.trim());
        if (line.length() == 0) {
            return;
        }

        String command = tokenizer.nextToken();
        List<String> tokens = new LinkedList<String>();
        while (tokenizer.hasMoreTokens()) {
            tokens.add(tokenizer.nextToken());
        }
        Args args = new Args(tokens.toArray(new String[tokens.size()]));

        if ("children".equals(command)) debugChildren(args);
        else if ("clear".equals(command)) debugClearBreakpoint(args);
        else if ("data".equals(command)) debugData(args);
        else if ("drop".equals(command)) debugDropFrame(args);
        else if ("eval".equals(command)) debugEval(args);
        else if ("eventstop".equals(command)) debugEventStop(args);
        else if ("exit".equals(command)) debugExit();
        else if ("frame".equals(command)) debugFrame(args);
        else if ("groups".equals(command)) debugGroups(args);
        else if ("popdata".equals(command)) debugPop(args);
        else if ("pushdata".equals(command)) debugPush(args);
        else if ("registers".equals(command)) debugRegisters(args);
        else if ("resume".equals(command)) debugResume(args);
        else if ("set".equals(command)) debugSetBreakpoint(args);
        else if ("setdata".equals(command)) debugSetData(args);
        else if ("setvar".equals(command)) debugSetVariable(args);
        else if ("stack".equals(command)) debugStack(args);
        else if ("stackdepth".equals(command)) debugStackDepth(args);
        else if ("state".equals(command)) debugState(args);
        else if ("step".equals(command)) debugStep(args);
        else if ("stepreturn".equals(command)) debugStepReturn(args);
        else if ("suspend".equals(command)) debugSuspend(args);
        else if ("threads".equals(command)) debugThreads();
        else if ("var".equals(command)) debugVar(args);
        else if ("vmresume".equals(command)) debugVMResume();
        else if ("vmsuspend".equals(command)) debugVMSuspend();
        else if ("watch".equals(command)) debugWatch(args);
        else {
            sendCommandResponse("error: invalid command\n");
        }
    }

    void debugChildren(Args args) {
        PDAThread thread = args.getThreadArg();
        if (thread == null) {
            sendCommandResponse("error: invalid thread\n");
            return;
        }

        int sfnumber = args.getNextIntArg();
        String var = args.getNextStringArg();
        
        Frame frame = sfnumber >= thread.fFrames.size()    
            ? thread.fCurrentFrame : thread.fFrames.get(sfnumber);

        String varDot = var + ".";
        List<String> children = new ArrayList<String>();
        for (String localVar : frame.fLocalVariables.keySet()) {
            if (localVar.startsWith(varDot) && localVar.indexOf('.', varDot.length() + 1) == -1) {
                children.add(localVar);
            }
        }

        Object value = frame.fLocalVariables.get(var); 
        if (value instanceof Object[]) {
            int size = ((Object[])value).length;
            for (int i = 0; i < size; i++) {
                children.add(var + "[" + Integer.toString(i) + "]");
            }
        }
        
        StringBuilder result = new StringBuilder();
        for (String child : children) {
            result.append(child);
            result.append('|');
        }
        result.append('\n');

        sendCommandResponse(result.toString());
    }
    
    void debugClearBreakpoint(Args args) {
        int line = args.getNextIntArg();

        fBreakpoints.remove(line);
        sendCommandResponse("ok\n");
    }

    private static Pattern fPackPattern = Pattern.compile("%([a-fA-F0-9][a-fA-F0-9])");

    void debugData(Args args) {
        PDAThread thread = args.getThreadArg();
        if (thread == null) {
            sendCommandResponse("error: invalid thread\n");
            return;
        }

        StringBuilder result = new StringBuilder();
        for (Object val : thread.fStack) {
            result.append(val);
            result.append('|');
        }
        result.append('\n');
        sendCommandResponse(result.toString());
    }

    void debugDropFrame(Args args) {
        PDAThread thread = args.getThreadArg();
        if (thread == null) {
            sendCommandResponse("error: invalid thread\n");
            return;
        }

        if (!thread.fFrames.isEmpty()) {
            thread.fCurrentFrame = thread.fFrames.remove(thread.fFrames.size() - 1);
        }
        thread.fCurrentFrame.fPC--;
        sendCommandResponse("ok\n");
        if (fSuspendVM != null) {
            sendDebugEvent("vmresumed drop", false);
            sendDebugEvent("vmsuspended " + thread.fID + " drop", false);
        } else {
            sendDebugEvent("resumed " + thread.fID + " drop", false);
            sendDebugEvent("suspended " + thread.fID + " drop", false);
        }
    }

    void debugEval(Args args) {
        if (fSuspendVM != null) {
            sendCommandResponse("error: cannot evaluate while vm is suspended\n");        
            return;
        }
        
        PDAThread thread = args.getThreadArg();
        if (thread == null) {
            sendCommandResponse("error: invalid thread\n");
            return;
        }
        
        if (thread.fSuspend == null) {
            sendCommandResponse("error: thread running\n");
            return;
        }
        
        StringTokenizer tokenizer = new StringTokenizer(args.getNextStringArg(), "|");
        tokenizer.countTokens();

        int numEvalLines = tokenizer.countTokens();
        thread.fThreadCode = new String[fCode.length + numEvalLines + 1];
        System.arraycopy(fCode, 0, thread.fThreadCode, 0, fCode.length);
        for (int i = 0; i < numEvalLines; i++) {
            String line = tokenizer.nextToken();
            StringBuilder lineBuf = new StringBuilder(line.length());
            Matcher matcher = fPackPattern.matcher(line);
            int lastMatchEnd = 0;
            while (matcher.find()) {
                lineBuf.append(line.substring(lastMatchEnd, matcher.start()));
                String charCode = line.substring(matcher.start() + 1, matcher.start() + 3);
                try {
                    lineBuf.append((char) Integer.parseInt(charCode, 16));
                } catch (NumberFormatException e) {
                }
                lastMatchEnd = matcher.end();
            }
            if (lastMatchEnd < line.length()) {
                lineBuf.append(line.substring(lastMatchEnd));
            }
            thread.fThreadCode[fCode.length + i] = lineBuf.toString();
        }
        thread.fThreadCode[fCode.length + numEvalLines] = "xyzzy";
        thread.fThreadLabels = mapLabels(fCode);

        thread.fSavedPC = thread.fCurrentFrame.fPC;
        thread.fCurrentFrame.fPC = fCode.length;
        thread.fPerformingEval = true;
        
        thread.fSuspend = null;
        
        sendCommandResponse("ok\n");

        sendDebugEvent("resumed " + thread.fID + " eval", false);
    }

    void debugEventStop(Args args) {
        String event = args.getNextStringArg();
        int stop = args.getNextIntArg();
        fEventStops.put(event, stop > 0);
        sendCommandResponse("ok\n");
    }

    void debugExit() {
        sendCommandResponse("ok\n");
        sendDebugEvent("terminated", false);
        System.exit(0);
    }

    void debugFrame(Args args) {
        PDAThread thread = args.getThreadArg();
        if (thread == null) {
            sendCommandResponse("error: invalid thread\n");
            return;
        }

        int sfnumber = args.getNextIntArg();
        Frame frame = null;
        if (sfnumber >= thread.fFrames.size()) {
            frame = thread.fCurrentFrame;
        } else {
            frame = thread.fFrames.get(sfnumber);
        }
        sendCommandResponse(printFrame(frame) + "\n");
    }

    void debugGroups(Args args) {
        TreeSet<String> groups = new TreeSet<String>();
        for (Register reg : fRegisters.values()) {
            groups.add(reg.fGroup);
        }
        StringBuilder response = new StringBuilder();
        for (String group : groups) {
            response.append(group);
            response.append('|');
        }
        response.append('\n');
        sendCommandResponse(response.toString());
    }

    void debugPop(Args args) {
        PDAThread thread = args.getThreadArg();
        if (thread == null) {
            sendCommandResponse("error: invalid thread\n");
            return;
        }

        thread.fStack.pop();
        sendCommandResponse("ok\n");
    }

    void debugPush(Args args) {
        PDAThread thread = args.getThreadArg();
        if (thread == null) {
            sendCommandResponse("error: invalid thread\n");
            return;
        }
        
        Object val = args.getNextIntOrStringArg();
        thread.fStack.push(val);
        sendCommandResponse("ok\n");
    }

    void debugRegisters(Args args) {
        String group = args.getNextStringArg();
        
        StringBuilder response = new StringBuilder();
        for (Register reg : fRegisters.values()) {
            if (group.equals(reg.fGroup)) {
                response.append(reg.fName);
                response.append(' ');
                response.append(reg.fIsWriteable);
                for (BitField bitField : reg.fBitFields.values()) {
                    response.append('|');
                    response.append(bitField.fName);
                    response.append(' ');
                    response.append(bitField.fBitOffset);
                    response.append(' ');
                    response.append(bitField.fBitCount);
                    response.append(' ');
                    for (Map.Entry<String, Integer> mnemonicEntry : bitField.fMnemonics.entrySet()) {
                        response.append(mnemonicEntry.getKey());
                        response.append(' ');
                        response.append(mnemonicEntry.getValue());
                        response.append(' ');
                    }
                }
                
                response.append('#');
            }
        }
        response.append('\n');
        sendCommandResponse(response.toString());
    }
    
    void debugResume(Args args) {
        PDAThread thread = args.getThreadArg();
        if (thread == null) {
            sendCommandResponse("error: invalid thread\n");
            return;
        } 
        if (fSuspendVM != null) {
            sendCommandResponse("error: cannot resume thread when vm is suspended\n");
            return;
        } 
        if (thread.fSuspend == null) {
            sendCommandResponse("error: thread already running\n");
            return;
        } 
        
        thread.fSuspend = null;
        sendDebugEvent("resumed " + thread.fID + " client", false);
        
        sendCommandResponse("ok\n");
    }

    void debugSetBreakpoint(Args args) {
        int line = args.getNextIntArg();
        int stopVM = args.getNextIntArg();
        
        fBreakpoints.put(line, stopVM != 0);
        sendCommandResponse("ok\n");
    }

    void debugSetData(Args args) {
        PDAThread thread = args.getThreadArg();
        if (thread == null) {
            sendCommandResponse("error: invalid thread\n");
            return;
        }
        
        int offset = args.getNextIntArg();
        Object val = args.getNextIntOrStringArg();

        if (offset < thread.fStack.size()) {
            thread.fStack.set(offset, val);
        } else {
            thread.fStack.add(0, val);
        }
        sendCommandResponse("ok\n");
    }

    void debugSetVariable(Args args) {
        PDAThread thread = args.getThreadArg();
        if (thread == null) {
            sendCommandResponse("error: invalid thread\n");
            return;
        }

        int sfnumber = args.getNextIntArg();
        String var = args.getNextStringArg();
        Object val = args.getNextIntOrStringArg();

        if (sfnumber >= thread.fFrames.size()) {
            thread.fCurrentFrame.set(var, val);
        } else {
            thread.fFrames.get(sfnumber).set(var, val);
        }
        sendCommandResponse("ok\n");
    }

    void debugStack(Args args) {
        PDAThread thread = args.getThreadArg();
        if (thread == null) {
            sendCommandResponse("error: invalid thread\n");
            return;
        }

        StringBuilder result = new StringBuilder();
        for (Frame frame : thread.fFrames) {
            result.append(printFrame(frame));
            result.append('#');
        }
        result.append(printFrame(thread.fCurrentFrame));
        result.append('\n');
        sendCommandResponse(result.toString());
    }

    void debugStackDepth(Args args) {
        PDAThread thread = args.getThreadArg();
        if (thread == null) {
            sendCommandResponse("error: invalid thread\n");
            return;
        }
        sendCommandResponse( Integer.toString(thread.fFrames.size() + 1) + "\n" );
    }


    /**
     * The stack frame output is: frame # frame # frame ... where each frame is:
     * filename | line number | function name | var | var | var | var ...
     */
    private String printFrame(Frame frame) {
        StringBuilder buf = new StringBuilder();
        buf.append(fFilename);
        buf.append('|');
        buf.append(frame.fPC);
        buf.append('|');
        buf.append(frame.fFunction);
        for (String var : frame.fLocalVariables.keySet()) {
            if (var.indexOf('.') >= 0) continue;
            if (var.indexOf('[') >= 0) {
                var = var.substring(0, var.indexOf('['));
            }
            buf.append('|');
            buf.append(var);
        }
        return buf.toString();
    }

    void debugState(Args args) {
        PDAThread thread = args.getThreadArg();
        String response = null;
        if (thread == null) {
            response = fSuspendVM == null ? "running" : fSuspendVM;
        } else if (fSuspendVM != null) {
            response = "vm";
        } else {
            response = thread.fSuspend == null ? "running" : thread.fSuspend;
        }
        sendCommandResponse(response + "\n");
    }
    
    void debugStep(Args args) {
        PDAThread thread = args.getThreadArg();
        if (thread == null) {
            sendCommandResponse("error: invalid thread\n");
            return;
        } 

        // Set suspend to null to allow the debug loop to exit back to the 
        // instruction loop and thus run an instruction. However, we want to 
        // come back to the debug loop right away, so the step flag is set to 
        // true which will cause the suspend flag to get set to true when we 
        // get to the next instruction.
        if (fSuspendVM != null) {
            // All threads are suspended, so suspend all threads again when 
            // step completes.
            fSuspendVM = null;
            fStepVM = true;
            // Also mark the thread that initiated the step to mark it as
            // the triggering thread when suspending.
            thread.fStep = true;
        } else {
            if (thread.fSuspend == null) {
                sendCommandResponse("error: thread already running\n");
                return;
            }
            thread.fSuspend = null;
            thread.fStep = true;
            sendDebugEvent("resumed " + thread.fID + " step", false);
        }
        sendCommandResponse("ok\n");
    }

    void debugStepReturn(Args args) {
        PDAThread thread = args.getThreadArg();
        if (thread == null) {
            sendCommandResponse("error: invalid thread\n");
            return;
        } 
        
        if (fSuspendVM != null) {
            fSuspendVM = null;
            fStepReturnVM = true;
            thread.fStepReturn = true;
        } else {
            if (thread.fSuspend == null) {
                sendCommandResponse("error: thread running\n");
                return;
            }
            thread.fSuspend = null;
            thread.fStepReturn = true;
            sendDebugEvent("resumed " + thread.fID + " step", false);
        }
        sendCommandResponse("ok\n");
    }

    void debugSuspend(Args args) {
        PDAThread thread = args.getThreadArg();
        if (thread == null) {
            sendCommandResponse("error: invalid thread\n");
            return;
        } 
        if (fSuspendVM != null) {
            sendCommandResponse("error: vm already suspended\n");
            return;
        }
        if (thread.fSuspend != null) {
            sendCommandResponse("error: thread already suspended\n");
            return;
        } 
        
        thread.fSuspend = "client";
        sendDebugEvent("suspended " + thread.fID + " client", false);
        sendCommandResponse("ok\n");
    }

    void debugThreads() {
        StringBuilder response = new StringBuilder();
        for (int threadId : fThreads.keySet()) {
            response.append(threadId);
            response.append(' ');
        }
        sendCommandResponse(response.toString().trim() + "\n");
    }

    void debugVar(Args args) {
        PDAThread thread = args.getThreadArg();
        if (thread == null) {
            sendCommandResponse("error: invalid thread\n");
            return;
        }

        int sfnumber = args.getNextIntArg();
        String var = args.getNextStringArg();

        Frame frame = sfnumber >= thread.fFrames.size()    
            ? thread.fCurrentFrame : thread.fFrames.get(sfnumber);
        
        Object val = frame.get(var);
        if (val == null) {
            sendCommandResponse("error: variable undefined\n");
        } else if (val instanceof Object[]) {
            sendCommandResponse("[" + Integer.toString(((Object[])val).length) + "]" + "\n");
        } else {
            sendCommandResponse(val.toString() + "\n");
        }
    }

    void debugVMResume() {
        if (fSuspendVM == null) {
            sendCommandResponse("error: vm already running\n");
            return;
        } 

        fSuspendVM = null;
        sendCommandResponse("ok\n");
    }

    void debugVMSuspend() {
        if (fSuspendVM != null) {
            sendCommandResponse("error: vm already suspended\n");
            return;
        }

        fSuspendVM = "client";
        sendCommandResponse("ok\n");
    }

    void debugWatch(Args args) {
        String funcAndVar = args.getNextStringArg();
        int flags = args.getNextIntArg();
        fWatchpoints.put(funcAndVar, flags);
        sendCommandResponse("ok\n");
    }

    void iAdd(PDAThread thread, Args args) {
        Object val1 = thread.fStack.pop();
        Object val2 = thread.fStack.pop();
        if (val1 instanceof Integer && val2 instanceof Integer) {
            int intVal1 = ((Integer) val1).intValue();
            int intVal2 = ((Integer) val2).intValue();
            thread.fStack.push(intVal1 + intVal2);
        } else {
            thread.fStack.push(-1);
        }
    }

    void iBranchNotZero(PDAThread thread, Args args) {
        Object val = thread.fStack.pop();
        if (val instanceof Integer && ((Integer) val).intValue() != 0) {
            String label = args.getNextStringArg();
            if (thread.fThreadLabels.containsKey(label)) {
                thread.fCurrentFrame.fPC = thread.fThreadLabels.get(label);
            } else {
                sendDebugEvent("no such label " + label, true);
                if (fEventStops.get("nosuchlabel")) {
                    fSuspendVM = thread.fID + " event nosuchlabel";
                    thread.fStack.push(val);
                    thread.fCurrentFrame.fPC--;
                }
            }
        }
    }

    void iCall(PDAThread thread, Args args) {
        String label = args.getNextStringArg();
        if (thread.fThreadLabels.containsKey(label)) {
            thread.fFrames.add(thread.fCurrentFrame);
            thread.fCurrentFrame = new Frame(label, thread.fThreadLabels.get(label));
        } else {
            sendDebugEvent("no such label " + label, true);
            if (fEventStops.get("nosuchlabel")) {
                fSuspendVM = thread.fID + " event nosuchlabel";
                thread.fCurrentFrame.fPC--;
            }
        }
    }

    void iDec(PDAThread thread, Args args) {
        Object val = thread.fStack.pop();
        if (val instanceof Integer) {
            val = ((Integer) val) - 1;
        }
        thread.fStack.push(val);
    }

    void iDef(PDAThread thread, Args args) {
        String type = args.getNextStringArg();

        String name = args.getNextStringArg();
        String regName = getRegisterPartOfName(name);
        String bitFieldName = getBitFieldPartOfName(name);

        if ("register".equals(type)) {
            Register reg = new Register(regName); 
            reg.fGroup = args.getNextStringArg();
            fRegisters.put(regName, reg);
            reg.fIsWriteable = args.getNextBooleanArg();
        } else if ("bitfield".equals(type)) {
            Register reg = fRegisters.get(regName);
            if (reg == null) return;
            BitField bitField = new BitField(bitFieldName);
            bitField.fBitOffset = args.getNextIntArg();
            bitField.fBitCount = args.getNextIntArg();
            reg.fBitFields.put(bitFieldName, bitField);
        } else if ("mnemonic".equals(type)) {
            Register reg = fRegisters.get(regName);
            if (reg == null) return;
            BitField bitField = reg.fBitFields.get(bitFieldName);
            if (bitField == null) return;
            bitField.fMnemonics.put(args.getNextStringArg(), args.getNextIntArg());
        }
        sendDebugEvent("registers", false);
    }

    private String getRegisterPartOfName(String name) {
        if (name.startsWith("$")) {
            int end = name.indexOf('.');
            end = end != -1 ? end : name.length();
            return name.substring(1, end);
        }
        return null;
    }

    private String getBitFieldPartOfName(String name) {
        int start = name.indexOf('.');
        if (name.startsWith("$") && start != -1) {
            return name.substring(start + 1, name.length());
        }
        return null;        
    }

    void iDup(PDAThread thread, Args args) {
        Object val = thread.fStack.pop();
        thread.fStack.push(val);
        thread.fStack.push(val);
    }
    
    void iExec(PDAThread thread, Args args) {
        String label = args.getNextStringArg();
        if (fLabels.containsKey(label)) {
            int id = fNextThreadId++;
            fThreads.put(id, new PDAThread(id, label, fLabels.get(label)));
            sendDebugEvent("started " + id, false);
        } else {
            sendDebugEvent("no such label " + label, true);
            if (fEventStops.get("nosuchlabel")) {
                thread.fSuspend = "event nosuchlabel";
                thread.fCurrentFrame.fPC--;
            }
        }
    }

    void iHalt(PDAThread thread, Args args) {
        thread.fRun = false;
    }

    void iOutput(PDAThread thread, Args args) {
        System.out.println(thread.fStack.pop());
    }

    void iPop(PDAThread thread, Args args) {
        String arg = args.getNextStringArg();
        if (arg.startsWith("$")) {
            String var = arg.substring(1);
            thread.fCurrentFrame.set(var, thread.fStack.pop());
            String key = thread.fCurrentFrame.fFunction + "::" + var;
            if (fWatchpoints.containsKey(key) && (fWatchpoints.get(key) & 2) != 0) {
                fSuspendVM = thread.fID + " watch write " + key;
            }
        } else {
            thread.fStack.pop();
        }
    }

    void iPush(PDAThread thread, Args args) {
        String arg = args.getNextStringArg();
        while (arg.length() != 0) {
            if (arg.startsWith("$")) {
                String var = arg.substring(1);
                Object val = thread.fCurrentFrame.get(var);
                if (val == null) val = "<undefined>";
                thread.fStack.push(val);
                String key = thread.fCurrentFrame.fFunction + "::" + var;
                if (fWatchpoints.containsKey(key) && (fWatchpoints.get(key) & 1) != 0) {
                    fSuspendVM = thread.fID + " watch read " + key;
                }
            } else {
                Object val = arg;
                try {
                    val = Integer.parseInt(arg);
                } catch (NumberFormatException e) {
                }
                thread.fStack.push(val);
            }
            
            arg = args.getNextStringArg();
        }
    }

    void iReturn(PDAThread thread, Args args) {
        if (!thread.fFrames.isEmpty()) {
            thread.fCurrentFrame = thread.fFrames.remove(thread.fFrames.size() - 1);
        } else {
            // Execution returned from the top frame, which means this thread
            // should exit.
            thread.fRun = false;
        }
    }

    void iVar(PDAThread thread, Args args) {
        String var = args.getNextStringArg();
        thread.fCurrentFrame.declare(var);
    }

    void iInternalEndEval(PDAThread thread, Args args) {
        Object result = thread.fStack.pop();
        thread.fThreadCode = fCode;
        thread.fThreadLabels = fLabels;
        thread.fCurrentFrame.fPC = thread.fSavedPC;
        sendDebugEvent("evalresult " + result, false);
        thread.fSuspend = "eval";
        thread.fPerformingEval = false;
    }

}
